using Engine.Graphics;
using Engine.Graphics.VertexStructures;
using SharpDX;
using SharpDX.Direct3D11;
using System.Collections.Generic;

namespace Engine.MeshObject
{
    public class BoundingBox : IRenderableObject
    {
        #region Public Fields

        public bool intersected;

        #endregion Public Fields

        #region Private Fields

        private Color4 color = new Color4(1.0f, 0.5f, 0.0f, 0.250f);
        private Engine engine;
        private Buffer ib;
        private BufferDescription indexBufferDescription;
        private short[] indices;
        private Color4 intersectColor = new Color4(0.75f, 1.0f, 0.0f, 0.25f);
        private Color4 intersectFailedColor = new Color4(1.0f, 0.0f, 0.0f, 0.25f);
        private Vector3 lowerRight;
        private Vector3[] normalForTransformedPolygons;
        private Vector3 position;
        private Vector3 positionCenter;
        private float rotation;
        private float scale;
        private System.Object thisLock = new System.Object();
        private CustomVertex.PositionColored[] TransformedVertices;
        private Vector3 upperLeft;
        private Buffer vb;
        private VertexBufferBinding vertexBufferBinding;
        private BufferDescription vertexBufferDescription;
        private CustomVertex.PositionColored[] vertices;

        #endregion Private Fields

        #region Public Constructors

        public BoundingBox(Engine engine, Vector3 positionOrigon, Vector3[] positions, float rotation, float scale)
            : this(engine)
        {
            this.position = positionOrigon;
            this.scale = scale;
            this.rotation = rotation;
            Update(positions, position, rotation, scale);
        }

        public BoundingBox(Engine engine, Vector3 upperLeft, Vector3 lowerRight)
            : this(engine)
        {
            this.positionCenter = (upperLeft + lowerRight) / 2.0f;
            this.upperLeft = upperLeft;
            this.lowerRight = lowerRight;
            UpdateBoundingBox();
        }

        public BoundingBox(Engine engine, Vector3 position, float size)
            : this(engine)
        {
            this.engine = engine;
            this.positionCenter = position;
            this.upperLeft = new Vector3(size, size, size);
            this.lowerRight = new Vector3(-size, -size, -size);
            UpdateBoundingBox();
        }

        public BoundingBox(Engine engine, BoundingBox boundingBox)
            : this(engine)
        {
            upperLeft = boundingBox.upperLeft;
            lowerRight = boundingBox.lowerRight;
            vertices = new CustomVertex.PositionColored[boundingBox.vertices.Length];
            for (int v = 0; v < vertices.Length; v++)
            {
                vertices[v] = boundingBox.vertices[v];
            }

            indices = new short[boundingBox.indices.Length];
            for (int i = 0; i < indices.Length; i++)
            {
                indices[i] = boundingBox.indices[i];
            }
        }

        public BoundingBox(Engine engine)
        {
            LoadShader();

            this.engine = engine;
            this.vertexBufferDescription = new BufferDescription()
            {
                BindFlags = BindFlags.VertexBuffer,
                CpuAccessFlags = 0,
                SizeInBytes = 0,
                Usage = ResourceUsage.Default
            };
            this.indexBufferDescription = new BufferDescription()
            {
                BindFlags = BindFlags.IndexBuffer,
                CpuAccessFlags = 0,
                SizeInBytes = 0,
                Usage = ResourceUsage.Default,
                OptionFlags = ResourceOptionFlags.None
            };

            this.vertices = new CustomVertex.PositionColored[8];
            this.scale = 1.0f;

            for (int v = 0; v < vertices.Length; v++)
            {
                vertices[v].Color = new Color4(System.Drawing.Color.Green.ToArgb());
            }

            indices = new short[36] {
                                    2,1,0, // Front Face
                                    2,3,1, // Front Face
                                    6,5,4, // Back Face
                                    7,5,6, // Back Face
                                    4,5,0, // Top Face
                                    5,2,0, // Top Face
                                    7,6,1, // Bottom Face
                                    3,7,1, // Bottom Face
                                    1,6,0, // Left Face
                                    0,6,4, // Left Face
                                    7,3,2, // Right Face
                                    7,2,5 // Right Face
                    };
            normalForTransformedPolygons = new Vector3[12];

            CreateIndexBuffer();
            UpdateVertexBuffer();
            //UpdateNormals();
        }

        #endregion Public Constructors

        #region Public Properties

        public Vector3 LowerRight
        {
            get { return lowerRight; }
            set
            {
                lowerRight = value;
                UpdateBoundingBox();
            }
        }

        public Vector3 Position { get { return this.position; } }

        public Vector3 PositionCenter { get { return this.positionCenter; } }

        public Vector3 UpperLeft
        {
            get { return upperLeft; }
            set
            {
                upperLeft = value;
                UpdateBoundingBox();
            }
        }

        #endregion Public Properties

        #region Public Methods

        public static bool IntersectsTriangle(Ray ray, Vector3 a, Vector3 b, Vector3 c, Vector3 normal, bool positiveSide, bool negativeSide, out float t)
        {
            t = 0;
            {
                float denom = Vector3.Dot(normal, ray.Direction);

                if (denom > float.Epsilon)
                {
                    if (!negativeSide)
                        return false;
                }
                else if (denom < -float.Epsilon)
                {
                    if (!positiveSide)
                        return false;
                }
                else
                {
                    return false;
                }

                t = Vector3.Dot(normal, a - ray.Position) / denom;

                if (t < 0)
                {
                    // Interersection is behind origin
                    return false;
                }
            }

            // Calculate the largest area projection plane in X, Y or Z.
            int i0, i1;
            {
                float n0 = System.Math.Abs(normal.X);
                float n1 = System.Math.Abs(normal.Y);
                float n2 = System.Math.Abs(normal.Z);

                i0 = 1;
                i1 = 2;

                if (n1 > n2)
                {
                    if (n1 > n0) i0 = 0;
                }
                else
                {
                    if (n2 > n0) i1 = 0;
                }
            }

            float[] A = { a.X, a.Y, a.Z };
            float[] B = { b.X, b.Y, b.Z };
            float[] C = { c.X, c.Y, c.Z };
            float[] R = { ray.Direction.X, ray.Direction.Y, ray.Direction.Z };
            float[] RO = { ray.Position.X, ray.Position.Y, ray.Position.Z };

            // Check the intersection point is inside the triangle.
            {
                float u1 = B[i0] - A[i0];
                float v1 = B[i1] - A[i1];
                float u2 = C[i0] - A[i0];
                float v2 = C[i1] - A[i1];
                float u0 = t * R[i0] + RO[i0] - A[i0];
                float v0 = t * R[i1] + RO[i1] - A[i1];

                float alpha = u0 * v2 - u2 * v0;
                float beta = u1 * v0 - u0 * v1;
                float area = u1 * v2 - u2 * v1;

                float EPSILON = 1e-3f;

                float tolerance = -EPSILON * area;

                if (area > 0)
                {
                    if (alpha < tolerance || beta < tolerance || alpha + beta > area - tolerance)
                        return false;
                }
                else
                {
                    if (alpha > tolerance || beta > tolerance || alpha + beta < area - tolerance)
                        return false;
                }
            }

            return true;
        }

        public void Dispose()
        {
            if (this.vb != null)
            {
                this.vb.Dispose();
                this.vb = null;
            }
            if (this.ib != null)
            {
                this.ib.Dispose();
                this.ib = null;
            }
        }

        public bool Intersect(Vector3 vNear, Vector3 vFar, out Vector3 NearestCuttingpoint, bool updateVertexBuffer)
        {
            NearestCuttingpoint = Vector3.Zero;
            intersected = false;
            List<Vector3> cuttingPoints = new List<Vector3>();
            Ray ray = new Ray();
            ray.Position = vNear;
            float length = GeoMath.DistanceBtw2Points(vNear, vFar);
            ray.Direction = vFar - vNear;
            ray.Direction.X /= length;
            ray.Direction.Y /= length;
            ray.Direction.Z /= length;
            float raylength;

            bool intersect = false;

            lock (this.thisLock)
                for (int i = 0; i < 12; i++)
                {
                    Vector3 normal = this.normalForTransformedPolygons[i];

                    intersect = IntersectsTriangle(ray,
                        this.TransformedVertices[indices[(i * 3)]].Position,
                        this.TransformedVertices[indices[(i * 3) + 1]].Position,
                        this.TransformedVertices[indices[(i * 3) + 2]].Position,
                        normal,
                        true, true, out raylength);

                    if (intersect && !float.IsInfinity(raylength))
                    {
                        cuttingPoints.Add(ray.Direction * raylength);
                    }
                }

            //find the nearest Cuttingpoiunt
            float nearestDistance = float.MaxValue;
            for (int i = 0; i < cuttingPoints.Count; i++)
            {
                float distance = GeoMath.DistanceBtw2Points(vNear, cuttingPoints[i]);
                if (distance < nearestDistance)
                {
                    nearestDistance = distance;
                    NearestCuttingpoint = cuttingPoints[i];
                    intersected = true;
                }
            }

            if (updateVertexBuffer)
            {
                // this.color = (cuttingPoints.Count > 0) ? this.intersectColor : this.intersectFailedColor;
                UpdateVertexBuffer();
            }

            return (cuttingPoints.Count > 0);
        }

        public Vector3 Normal2Poly(Vector3 v1, Vector3 v2, Vector3 v3)
        {
            Vector3 normal;

            normal.X = ((v1.Y - v2.Y) * (v1.Z - v3.Z) - (v1.Z - v2.Z) * (v1.Y - v3.Y));
            normal.Y = -((v1.X - v2.X) * (v1.Z - v3.Z) - (v1.Z - v2.Z) * (v1.X - v3.X));
            normal.Z = ((v1.X - v2.X) * (v1.Y - v3.Y) - (v1.Y - v2.Y) * (v1.X - v3.X));

            // normalize
            float Betrag = 1.1f * (float)System.Math.Sqrt(((normal.X * normal.X) + (normal.Y * normal.Y) + (normal.Z * normal.Z)));
            if (Betrag == 0)
                Betrag = 1.0f;

            //Betrag *= 3;
            normal.X /= Betrag;
            normal.Y /= Betrag;
            normal.Z /= Betrag;

            return normal;
        }

        /// <summary>
        /// In Wireframe:
        /// Best Way to Render with Z-Buffer and Alpha Disable.
        /// In Solid Fillmode: When transparent BoundingBox should be drawn. You must draw the box over all others objects.
        /// </summary>
        public void Render(int renderStep)
        {
            lock (thisLock)
                if (vb != null)
                {
                    DeviceContext deviceContext = this.engine.Device.ImmediateContext;
                    lock (deviceContext)
                    {
                        this.engine.Grafic.ShaderManager.SetShader(this.engine.Grafic.ShaderManager.ColorShader);
                        deviceContext.InputAssembler.SetVertexBuffers(0, this.vertexBufferBinding);
                        deviceContext.InputAssembler.SetIndexBuffer(this.ib, SharpDX.DXGI.Format.R16_UInt, 0);
                        deviceContext.InputAssembler.PrimitiveTopology = SharpDX.Direct3D.PrimitiveTopology.TriangleList;

                        deviceContext.DrawIndexed(36, 0, 0);
                    }
                }
        }

        public void Update(Vector3[] positions, Vector3 position, float rotation, float scale)
        {
            this.upperLeft.X = float.MinValue;
            this.upperLeft.Y = float.MinValue;
            this.upperLeft.Z = float.MinValue;

            this.lowerRight.X = float.MaxValue;
            this.lowerRight.Y = float.MaxValue;
            this.lowerRight.Z = float.MaxValue;

            for (int p = 0; p < positions.Length; p++)
            {
                if (positions[p].X > this.upperLeft.X)
                {
                    this.upperLeft.X = positions[p].X;
                }
                if (positions[p].Y > this.upperLeft.Y)
                {
                    this.upperLeft.Y = positions[p].Y;
                }
                if (positions[p].Z > this.upperLeft.Z)
                {
                    this.upperLeft.Z = positions[p].Z;
                }

                if (positions[p].X < this.lowerRight.X)
                {
                    this.lowerRight.X = positions[p].X;
                }
                if (positions[p].Y < this.lowerRight.Y)
                {
                    this.lowerRight.Y = positions[p].Y;
                }
                if (positions[p].Z < this.lowerRight.Z)
                {
                    this.lowerRight.Z = positions[p].Z;
                }
            }

            this.position = position;
            this.rotation = rotation;
            this.scale = scale;

            UpdateBoundingBox();
        }

        public void UpdateBox(float highestX, float lowestX, float highestY, float lowestY, float highestZ, float lowestZ)
        {
            lock (thisLock)
            {
                this.lowerRight = new Vector3(highestX, highestY, lowestZ);
                this.upperLeft = new Vector3(lowestX, lowestY, highestZ);
                UpdateBoundingBox();
            }
        }

        public void UpdateVertexBuffer()
        {
            lock (this.thisLock)
            {
                rotation = rotation * (float)(System.Math.PI / 180);
                Matrix matrix = new Matrix();

                Vector3 _scale = new Vector3();
                _scale.X = scale;
                _scale.Y = scale;
                _scale.Z = scale;

                Buffer newVertexBufferIfSizeChanged;

                CustomVertex.PositionColored[] tempVertices;
                tempVertices = new CustomVertex.PositionColored[this.vertices.Length];
                this.vertices.CopyTo(tempVertices, 0);

                for (int v = 0; v < tempVertices.Length; v++)
                {
                    Vector3 vector = tempVertices[v].Position;
                    matrix = Matrix.RotationY(rotation);
                    vector = Vector3.TransformCoordinate(vector, matrix);
                    vector = (vector * scale) + this.position;
                    tempVertices[v].Position = vector;
                    tempVertices[v].Color = (intersected) ? this.intersectColor : this.color;
                }

                int newSizeVertexBuffer = CustomVertex.PositionColored.SizeInBytes() * tempVertices.Length;

                // Update/Create VertexBuffer if needed
                if (this.vb == null || newSizeVertexBuffer != this.vb.Description.SizeInBytes)
                {
                    this.vertexBufferDescription.SizeInBytes = newSizeVertexBuffer;

                    lock (this.engine.Grafic.Device.ImmediateContext)
                    {
                        newVertexBufferIfSizeChanged = Buffer.Create(this.engine.Device, tempVertices, this.vertexBufferDescription);

                        Buffer vbDelete = this.vb;

                        this.vb = newVertexBufferIfSizeChanged;
                        this.vb.DebugName = "BoundingBox VB";

                        this.vertexBufferBinding = new VertexBufferBinding(this.vb, CustomVertex.PositionColored.SizeInBytes(), 0);

                        if (vbDelete != null)
                            vbDelete.Dispose();

                        vbDelete = null;
                    }
                }
                else
                {
                    lock (this.engine.Device.ImmediateContext)
                    {
                        this.engine.Device.ImmediateContext.UpdateSubresource(tempVertices, this.vb);
                    }
                }

                this.TransformedVertices = tempVertices;

                UpdateNormals();
            }
        }

        #endregion Public Methods

        #region Private Methods

        private void CreateIndexBuffer()
        {
            int newSizeIndexBuffer = this.indices.Length * sizeof(short);

            if (this.ib == null || this.ib.Description.SizeInBytes != newSizeIndexBuffer)
            {
                Buffer tempIndexBuffer = this.ib;

                this.indexBufferDescription.SizeInBytes = newSizeIndexBuffer;
                lock (this.engine.Grafic.Device.ImmediateContext)
                {
                    Buffer newIndexBuffer = Buffer.Create(this.engine.Grafic.Device, this.indices, this.indexBufferDescription);
                    newIndexBuffer.DebugName = "indexbuffer of Boundingbox.";
                    this.ib = newIndexBuffer;

                    if (tempIndexBuffer != null)
                        tempIndexBuffer.Dispose();
                    tempIndexBuffer = null;
                }
            }
            else
                lock (this.engine.Device.ImmediateContext)
                    this.engine.Grafic.Device.ImmediateContext.UpdateSubresource(this.indices, this.ib);
        }

        private void LoadShader()
        {
        }

        private void UpdateBoundingBox()
        {
            float highestX = this.upperLeft.X;// +2.0f;
            float highestY = this.upperLeft.Y;// + 2.0f;
            float highestZ = this.upperLeft.Z;// + 2.0f;
            float lowestX = this.lowerRight.X;// - 2.0f;
            float lowestY = this.lowerRight.Y;// - 2.0f;
            float lowestZ = this.lowerRight.Z;// - 2.0f;

            vertices[0].Position = new Vector3(lowestX, lowestY, highestZ);
            vertices[1].Position = new Vector3(lowestX, highestY, highestZ);
            vertices[2].Position = new Vector3(highestX, lowestY, highestZ);
            vertices[3].Position = new Vector3(highestX, highestY, highestZ);

            vertices[4].Position = new Vector3(lowestX, lowestY, lowestZ);
            vertices[5].Position = new Vector3(highestX, lowestY, lowestZ);
            vertices[6].Position = new Vector3(lowestX, highestY, lowestZ);
            vertices[7].Position = new Vector3(highestX, highestY, lowestZ);

            this.positionCenter = Vector3.Multiply(this.lowerRight + this.upperLeft, 0.5f);

            if (vb != null)
            {
                UpdateVertexBuffer();
            }
        }

        private void UpdateNormals()
        {
            lock (this.thisLock)
                for (int i = 0; i < 12; i++)
                {
                    this.normalForTransformedPolygons[i] = Normal2Poly(
                        this.TransformedVertices[indices[(i * 3)]].Position,
                        this.TransformedVertices[indices[(i * 3) + 1]].Position,
                        this.TransformedVertices[indices[(i * 3) + 2]].Position
                        );
                }
        }

        #endregion Private Methods
    }
}